# ORM으로 관련 개체 작업하기
이번 챕터에서는 다른 객체를 참조하는 매핑된 객체와 상호작용하는 방식인 또 하나의 필수적인 ORM 개념을 다룰 것입니다.
relationship()
은 매핑된 두 객체 간의 관계를 정의하며, 자기 참조관계라고도 합니다.
기본적인 구조를 위해 Column
매핑 및 기타 지시문을 생략하고 짧은 형식으로 relationship()
을 설명드리겠습니다.
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'user_account'
# ... Column mappings
addresses = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = 'address'
# ... Column mappings
user = relationship("User", back_populates="addresses")
위 구조를 보면 User
객체에는 addresses
변수, Address
객체에는 user
라는 변수가 있습니다.
공통적으로 relationship
객체로 생성되어져 있는 것을 볼 수 있습니다.
이는 실제 데이터베이스에 컬럼으로 존재하는 변수는 아니지만 코드 상에서 쉽게 접근할 수 있도록 하기 위해 설정 되었습니다.
즉, User
객체에서 Address
객체로 쉽게 찾아갈 수 있게 해줍니다.
또한 relationship
선언시 파라미터로 back_populates
항목은 반대의 상황 즉,
Address
객체에서 User
객체를 찾아 갈 수 있게 해줍니다.
관계형으로 보았을 경우 1 : N 관계를 자연스럽게 N : 1 관계로 해주는 설정입니다.
다음 섹션에서 relationship()
객체의 인스턴스가 어떤 역할을 하는지, 동작하는지 보겠습니다.
# 관계된 객체 사용하기
새로운 User
객체를 만들면 .addresses
컬렉션이 나타나는데 List
객체임을 알 수 있습니다.
>>> u1 = User(name='pkrabs', fullname='Pearl Krabs')
>>> u1.addresses
[]
list.append()
를 사용하여 Address
객체를 추가할 수 있습니다.
>>> a1 = Address(email_address="pear1.krabs@gmail.com")
>>> u1.addresses.append(a1)
# u1.addresses 컬렉션에 새로운 Address 객체가 포함되었습니다.
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]
Address
객체를 인스턴스 User.addresses
컬렉션과 연관시켰다면 변수 u1
에는 또 다른 동작이 발생하는데,
User.addresses
와 Address.user
관계가 동기화 되어
User
객체에서Address
이동할 수 있을 뿐만 아니라Address
객체에서 다시User
객체로 이동할 수도 있습니다.
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')
두개의 relationshiop()
객체 간의 relationship.back_populates
을 사용한 동기화 결과입니다.
매개변수 relationshiop()
는 보완적으로 할당/목록 변형이 발생할때 다른 변수로 지정할 수 있습니다.
다른 Address
객체를 생성하고 해당 Address.user
속성에 할당하면 해당 객체 Address
에 대한 User.addresses
컬렉션의 일부가 되는것도 확인 할 수 있습니다.
>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]
우리는 실제로 객체(Address
)에 선언된 속성처럼 user
의 키워드 인수로 u1
변수를 사용했습니다.
다음 사실 이후에 속성을 할당하는 것과 같습니다.
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
# Session
에 객체 캐스케이딩
이제 메모리의 양방향 구조와 연결된 두 개의 User
, Address
객체가 있지만 이전에 ORM으로 행 삽입하기 에서 언급했듯이 이러한 객체는 객체와 연결될 때까지 일시적인 Session
상태에 있습니다.
우리는 Session.add()
를 사용하고, User
객체에 메서드를 적용할 때 관련 Address
객체도 추가된다는 점을 확인해 볼 필요가 있습니다.
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True
세 개의 객체는 이제 보류 상태에 있으며, 이는 INSERT 작업이 진행되지 않았음을 의미합니다.
세 객체는 모두 기본 키가 할당되지 않았으며, 또한 a1 및 a2 객체에는 열(user_id
)을 참조 속성이 있습니다.
이는 객체가 아직 실제 데이터베이스 연결되지 않았기 때문입니다.
>>> print(u1.id)
None
>>> print(a1.user_id)
None
데이터베이스에 저장해봅시다.
>>> session.commit()
구현한 코드를 SQL 쿼리로 동작을 해본다면 이와 같습니다.
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('pkrabs', 'Pearl Krabs')
INSERT INTO address (email_address, user_id) VALUES (?, ?)
[...] ('pearl.krabs@gmail.com', 6)
INSERT INTO address (email_address, user_id) VALUES (?, ?)
[...] ('pearl@aol.com', 6)
COMMIT
session
을 사용하여 SQL의 INSERT, UPDATE, DELETE 문을 자동화할 수 있습니다.
마지막으로 Session.commit()
을 실행하여 모든 단계를 올바른 순서로 호출되며 user_account
에 address.user_id
기본키가 적용됩니다.
# 관계 로드
Session.commit()
을 호출한 이후에는 u1
객체에 생성된 기본 키를 볼 수 있게됩니다.
>>> u1.id
6
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (6,)
다음처럼 u1.addresses
에 연결된 객체들에도 id
가 들어와있는 것을 볼 수 있습니다.
해당 객체를 검색하기 위해 우리는 lazy load 방식으로 볼 수 있습니다.
lazy loading : 누군가 해당 정보에 접근하고자 할때 그때 SELECT문을 날려서 정보를 충당하는 방식. 즉, 그때그때 필요한 정보만 가져오는 것입니다.
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id AS address_id, address.email_address AS address_email_address,
address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (6,)
SQLAlchemy ORM의 기본 컬렉션 및 관련 특성은 lazy loading 입니다. 즉, 한번 relationship
된 컬렉션은 데이터가 메모리에 존재하는 한 계속 접근을 사용할 수 있습니다.
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
lazy loading은 최적화를 위한 명시적인 단계를 수행하지 않으면 비용이 많이 들 수 있지만, 적어도 lazy loading은 중복 작업을 수행하지 않도록 최적화되어 있습니다.
u1.addresses
의 컬렉션에 a1
및 a2
객체들 또한 볼 수 있습니다.
>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')
relationship
개념에 대한 추가 소개는 이 섹션의 후반부에 더 설명드리겠습니다.
# 쿼리에서 relationship
사용하기
이 섹션에서는 relationship()
이 SQL 쿼리 구성을 자동화하는데 도움이 되는 여러 가지 방법을 소개합니다.
# relationship()
을 사용하여 조인하기
FROM절과 JOIN명시하기 및 WHERE절 섹션에서는 Select.join()
및 Select.join_from()
메서드를 사용하여 SQL JOIN을 구성하였습니다.
테이블간에 조인하는 방법을 설명하기 위해 이러한 메서드는 두 테이블을 연결하는 ForeignKeyConstraint
객체가 있는지 여부에 따라 ON 절을 유추하거나 특정 ON 절을 나타내는 SQL Expression 구문을 제공 할 수 있습니다.
relationship()
객체를 사용하여 join의 ON 절을 설정할 수 있습니다.
relationship()
에 해당하는 객체는 Select.join()
의 단일 인수로 전달될 수 있으며,
right join과 ON 절을 동시에 나타내는 역할을 합니다.
>>> print(
... select(Address.email_address).
... select_from(User).
... join(User.addresses)
... )
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
매핑된 relationship()
있는 경우 Select.join()
또는 Select.join_from()
지정하지 않을 경우 ON 절은 사용되지 않습니다.
즉, user
및 Address
객체의 relationship()
객체가 아니라 매핑된 두 테이블 객체 간의 ForeignKeyConstraint
로 인해 작동합니다.
>>> print(
... select(Address.email_address).
... join_from(User, Address)
... )
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
# 별칭(aliased)을 사용하여 조인하기
relationship()
을 사용하여 SQL JOIN을 구성하는 경우 [PropComparator.of_type()
] 사용하여 조인 대상이 aliased()
이 되는 사용 사례가 적합합니다. 그러나 relationship()
를 사용하여 [ORM Entity Aliases
]에 설명된 것과 동일한 조인을 구성합니다.
>>> from sqlalchemy.orm import aliased
>>> address_alias_1 = aliased(Address)
>>> address_alias_2 = aliased(Address)
>>> print(
... select(User).
... join_from(User, address_alias_1).
... where(address_alias_1.email_address == 'patrick@aol.com').
... join_from(User, address_alias_2).
... where(address_alias_2.email_address == 'patrick@gmail.com')
... )
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
JOIN address AS address_1 ON user_account.id = address_1.user_id
JOIN address AS address_2 ON user_account.id = address_2.user_id
WHERE address_1.email_address = :email_address_1
AND address_2.email_address = :email_address_2
relationship()
을 사용하여 aliased()
에서 조인을 직접 사용할 수 있습니다.
>>> user_alias_1 = aliased(User)
>>> print(
... select(user_alias_1.name).
... join(user_alias_1.addresses)
... )
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account_1.name
FROM user_account AS user_account_1
JOIN address ON user_account_1.id = address.user_id
# ON 조건 확대
relation()
으로 생성된 ON 절에 조건을 추가할 수 있습니다. 이 기능은 관계된 경로에 대한 특정 조인의 범위를 신속하게 제한하는 방법뿐만 아니라 마지막 섹션에서 소개하는 로더 전략 구성과 같은 사용 사례에도 유용합니다.
PropComparator.and_()
메서드는 AND를 통해 JOIN의 ON 절에 결합되는 일련의 SQL 식을 위치적으로 허용합니다. 예를 들어,
User
및 Address
을 활용하여 ON 기준을 특정 이메일 주소로만 제한하려는 경우 이와 같습니다.
>>> stmt = (
... select(User.fullname).
... join(User.addresses.and_(Address.email_address == 'pearl.krabs@gmail.com'))
... )
>>> session.execute(stmt).all()
[('Pearl Krabs',)]
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.fullname
FROM user_account
JOIN address ON user_account.id = address.user_id AND address.email_address = ?
[...] ('pearl.krabs@gmail.com',)
# EXISTS has() , and()
EXISTS 서브쿼리들 섹션에서는 SQL EXISTS 키워드를 스칼라 서브 쿼리, 상호연관 쿼리 섹션과 함께 소개했습니다.
relationship()
은 관계 측면에서 공통적으로 서브쿼리를 생성하는데 사용할 수 있는 일부 도움을 제공합니다.
User.addresses
와 같은 1:N (one-to-many) 관계의 경우 PropComparator.any()
를 사용하여 user_account
테이블과 다시 연결되는 주소 테이블에 서브쿼리를 생성할 수 있습니다. 이 메서드는 하위 쿼리와 일치하는 행을 제한하는 선택적 WHERE 기준을 허용합니다.
>>> stmt = (
... select(User.fullname).
... where(User.addresses.any(Address.email_address == 'pearl.krabs@gmail.com'))
... )
>>> session.execute(stmt).all()
[('Pearl Krabs',)]
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.fullname
FROM user_account
WHERE EXISTS (SELECT 1
FROM address
WHERE user_account.id = address.user_id AND address.email_address = ?)
[...] ('pearl.krabs@gmail.com',)
이와 반대로 관련된 데이터가 없는 객체를 찾는 것은 ~User.addresses.any()
을 사용하여 User
객체에 검색하는 방법입니다.
>>> stmt = (
... select(User.fullname).
... where(~User.addresses.any())
... )
>>> session.execute(stmt).all()
[('Patrick McStar',), ('Squidward Tentacles',), ('Eugene H. Krabs',)]
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.fullname
FROM user_account
WHERE NOT (EXISTS (SELECT 1
FROM address
WHERE user_account.id = address.user_id))
[...] ()
PropComparator.has()
메서드는 PropComparator.any()
와 비슷한 방식으로 작동하지만, N:1 (Many-to-one) 관계에 사용됩니다.
예시로 "pearl"에 속하는 모든 Address
객체를 찾으려는 경우 이와 같습니다.
>>> stmt = (
... select(Address.email_address).
... where(Address.user.has(User.name=="pkrabs"))
... )
>>> session.execute(stmt).all()
[('pearl.krabs@gmail.com',), ('pearl@aol.com',)]
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.email_address
FROM address
WHERE EXISTS (SELECT 1
FROM user_account
WHERE user_account.id = address.user_id AND user_account.name = ?)
[...] ('pkrabs',)
# 관계 연산자
relationship()
와 함께 제공되는 SQL 생성 도우미에는 다음과 같은 몇 가지 종류가 있습니다.
- N : 1 (Many-to-one) 비교
특정 객체 인스턴스를 N : 1 관계와 비교하여 대상 엔티티의 외부 키가 지정된 객체의 기본 키값과 일치하는 행을 선택할 수 있습니다.
>>> print(select(Address).where(Address.user == u1))
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id, address.email_address, address.user_id
FROM address
WHERE :param_1 = address.user_id
- NOT N : 1 (Many-to-one) 비교
같지 않은 연산자(!=)를 사용할 수 있습니다.
>>> print(select(Address).where(Address.user != u1))
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id, address.email_address, address.user_id
FROM address
WHERE address.user_id != :user_id_1 OR address.user_id IS NULL
- 객체가 1 : N (one-to-many) 컬렉션에 포함되어있는지 확인하는 방법입니다.
>>> print(select(User).where(User.addresses.contains(a1)))
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.id = :param_1
- 객체가 1 : N 관계에서 특정 상위 항목에 있는지 확인하는 방법입니다.
with_parent()
은 주어진 상위 항목이 참조하는 행을 반환하는 비교를 생성합니다. 이는 == 연산자를 사용하는 것과 동일합니다.
>>> from sqlalchemy.orm import with_parent
>>> print(select(Address).where(with_parent(u1, User.addresses)))
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id, address.email_address, address.user_id
FROM address
WHERE :param_1 = address.user_id
# Loading relationshiop의 종류
관계 로드
섹션에서는 매핑된 객체 인스턴스로 작업할 때 relationship()
을 사용하여 매핑된 특성에 엑세스하면 이 컬렉션에 있어야 하는 객체를 로드하며, 컬렉션이 채워지지 않은 경우 lazy load
가 발생한다는 개념을 도입했습니다.
Lazy loading 방식은 가장 유명한 ORM 패턴 중 하나이며, 가장 논란이 많은 ORM 패턴이기도 합니다.
메모리에 있는 수십개의 ORM 객체가 각각 소수의 언로드 속성을 참조하는 경우, 객체의 일상적인 조작은 누적이 될 수 있는 많은 문제(N+1 Problem
)를 암묵적으로 방출될 수 있습니다. 이러한 암시적 쿼리는 더 이상 사용할 수 없는 데이터베이스 변환을 시도할 때 또는 비동기화 같은 대체 동시성 패턴을 사용할 때 실제로 전혀 작동하지 않을 수 있습니다.
N + 1 Problem
이란?
쿼리 1번으로 N건의 데이터를 가져왔는데 원하는 데이터를 얻기 위해 이 N건의 데이터를 데이터 수 만큼 반복해서 2차적으로 쿼리를 수행하는 문제입니다.
lazy loading 방식은 사용 중인 동시성 접근법과 호환되고 다른 방법으로 문제를 일으키지 않을 때 매우 인기있고 유용한 패턴입니다. 이러한 이유로 SQLAlchemy의 ORM은 이러한 로드 동작을 제허하고 최적화할 수 있는 기능에 중점을 둡니다.
무엇보다 ORM의 lazy loading 방식을 효과적으로 사용하는 첫 번째 단계는 Application을 테스트하고 SQL을 확인하는 것입니다.
Session
에서 분리된 객체에 대해 로드가 부적절하게 발생하는 경우, Loading relationship의 종류
사용을 검토해야 합니다.
Select.options()
메서드를 사용하여 SELECT 문과 연결할 수 있는 객체로 표시됩니다.
for user_obj in session.execute(
select(User).options(selectinload(User.addresses))
).scalars():
user_obj.addresses # access addresses collection already loaded
relationship.lazy
를 사용하여 relationship()
의 기본값으로 구성할 수도 있습니다.
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'user_account'
addresses = relationship("Address", back_populates="user", lazy="selectin")
가장 많이 사용되는 loading 방식 몇 가지를 소개합니다.
참고
관계 로딩 기법의 2가지 기법
Configuring Loader Strategies at Mapping Time
-relationship()
구성에 대한 세부정보
Relationship Loading with Loader Options
- 로더에 대한 세부정보
# Select IN loading 방식
최신 SQLAlchemy에서 가장 유용한 로딩방식 옵션은 selectinload()
입니다. 이 옵션은 관련 컬렉션을 참조하는 객체 집합의 문제인 가장 일반적인 형태의 "N + 1 Problem"문제를 해결합니다.
대부분의 경우 JOIN 또는 하위 쿼리를 도입하지 않고 관련 테이블에 대해서만 내보낼 수 있는 SELET 양식을 사용하여 이 작업을 수행합니다. 또한 컬렉션이 로드되지 않은 상위 객체에 대한 쿼리만 수행합니다.
아래 예시는 User
객체와 관련된 Address
객체를 selectinload()
하여 보여줍니다.
Session.execute()
호출하는 동안 데이터베이스에서는 두 개의 SELECT 문이 생성되고 두 번째는 관련 Address
객체를 가져오는 것입니다.
>>> from sqlalchemy.orm import selectinload
>>> stmt = (
... select(User).options(selectinload(User.addresses)).order_by(User.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})")
spongebob (spongebob@sqlalchemy.org)
sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick ()
squidward ()
ehkrabs ()
pkrabs (pearl.krabs@gmail.com, pearl@aol.com)
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account ORDER BY user_account.id
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id,
address.email_address AS address_email_address
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
# Joined Loading 방식
Joined Loading
은 SQLAlchemy에서 가장 오래됬으며, 이 방식은 eager loading의 일종으로 joined eager loading
이라고도 합니다. N : 1 관계의 객체를 로드하는 데 가장 적합하며,
relationship()
에 명시된 테이블을 SELECT JOIN하여 모든 테이블의 데이터들을 한꺼번에 가져오는 방식으로 Address
객체에 연결된 사용자가 있는 다음과 같은 경우에 OUTER JOIN이 아닌 INNER JOIN을 사용할 수 있습니다.
>>> from sqlalchemy.orm import joinedload
>>> stmt = (
... select(Address).options(joinedload(Address.user, innerjoin=True)).order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1,
user_account_1.name, user_account_1.fullname
FROM address
JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
ORDER BY address.id
[...] ()
joinedload()
는 1 : N 관계를 의미하는 컬렉션에도 사용되지만 중접 컬렉션 및 더 큰 컬렉션이므로 selectinload()
처럼 사례별로 평가해야 하는 것과 같은 다른 옵션과 비교 합니다.
SELECT 쿼리문의 WHERE 및 ORDER BY 기준은 joinload()에 의해 렌더링된 테이블을 대상으로 하지 않는다는 점에 유의하는 것이 중요합니다. 위 SQL 쿼리에서 직접 주소를 지정할 수 없는 *익명 별칭**이 user_account
테이블에 적용된 것을 볼 수 있습니다. 이 개념은 Zen of joined Eager Loading
섹션에서 더 자세히 설명합니다.
joinedload()
에 의해 ON 절은 이전 ON 조건 확대
에서 설명한 방법 joinedload()
을 사용하여 직접 영향을 받을 수 있습니다.
참고
일반적인 경우에는 "N + 1 problem"가 훨씬 덜 만연하기 때문에 다대일 열망 로드가 종종 필요하지 않다는 점에 유의하는 것이 중요합니다. 많은 객체가 모두 동일한 관련 객체를 참조하는 경우(예:Address
각각 동일한 참조하는 많은 객체) 일반 지연 로드를 사용하여User
객체에 대해 SQL이 한 번만 내보내 집니다. 지연 로드 루틴은Session
가능한 경우 SQL을 내보내지 않고 현재 기본 키로 관련 객체를 조회 합니다.
# Explicit Join + Eager load 방식
일반적인 사용 사례는 contains_eager()
옵션을 사용하며, 이 옵션은 JOIN을 직접 설정했다고 가정하고 대신 COLUMNS 절의 추가 열이 반환된 각 객체의 관련 속성에 로드해야 한다는 점을 제외하고는 joinedload()
와 매우 유사합니다.
>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
... select(Address).
... join(Address.user).
... where(User.name == 'pkrabs').
... options(contains_eager(Address.user)).order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.id, user_account.name, user_account.fullname,
address.id AS id_1, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? ORDER BY address.id
[...] ('pkrabs',)
위에서 user_account.name
을 필터링하고 user_account
의 반환된 Address.user
속성으로 로드했습니다.
joinedload()
를 별도로 적용했다면 불필요하게 두 번 조인된 SQL 쿼리가 생성되었을 것입니다.
>>> stmt = (
... select(Address).
... join(Address.user).
... where(User.name == 'pkrabs').
... options(joinedload(Address.user)).order_by(Address.id)
... )
>>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT address.id, address.email_address, address.user_id,
user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname
FROM address JOIN user_account ON user_account.id = address.user_id
LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
WHERE user_account.name = :name_1 ORDER BY address.id
참고
관계 로딩 기법의 2가지 기법
Zen of joined Eager Loading
- 해당 로딩 방식에 대한 세부정보
Routing Explicit Joins/Statements into Eagerly Loaded Collections
- usingcontains_eager()
# 로더 경로 설정
PropComparator.and_()
방법은 실제로 대부분의 로더 옵션에서 일반적으로 사용할 수 있습니다.
예를 들어 sqlalchemy.org
도메인에서 사용자 이름과 이메일 주소를 다시 로드하려는 경우 selectinload()
전달된 인수에 PropComparator.and_()
를 적용하여 다음 조건을 제한할 수 있습니다.
>>> from sqlalchemy.orm import selectinload
>>> stmt = (
... select(User).
... options(
... selectinload(
... User.addresses.and_(
... ~Address.email_address.endswith("sqlalchemy.org")
... )
... )
... ).
... order_by(User.id).
... execution_options(populate_existing=True)
... )
>>> for row in session.execute(stmt):
... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})")
spongebob ()
sandy (sandy@squirrelpower.org)
patrick ()
squidward ()
ehkrabs ()
pkrabs (pearl.krabs@gmail.com, pearl@aol.com)
위 코드는 다음 쿼리를 실행하는 것과 같습니다.
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account ORDER BY user_account.id
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id,
address.email_address AS address_email_address
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
AND (address.email_address NOT LIKE '%' || ?)
[...] (1, 2, 3, 4, 5, 6, 'sqlalchemy.org')
위에서 매우 중요한 점은 .execution_options(populate_existing=True)
옵션이 추가되었다는 점 입니다.
행을 가져올 때 적용되는 이 옵션은 로더 옵션이 이미 로드된 객체의 기존 컬렉션 내용을 대체해야 함을 나타냅니다.
Session
객체로 반복 작업하므로 위에서 로드되는 객체는 본 튜토리얼의 ORM 섹션 시작 시 처음 유지되었던 것과 동일한 Python 인스턴스입니다.
# raise loading 방식
raiseload()
옵션은 일반적으로 느린 대신 오류를 발생시켜 N + 1 문제가 발생하는 것을 완전히 차단하는데 사용됩니다.
예로 두 가지 변형 모델이 있습니다. SQL이 필요한 lazy load
와 현재 Session
만 참조하면 되는 작업을 포함한 모든 "load" 작업을 차단하는 raiseload.sql_only
옵션입니다.
class User(Base):
__tablename__ = 'user_account'
# ... Column mappings
addresses = relationship("Address", back_populates="user", lazy="raise_on_sql")
class Address(Base):
__tablename__ = 'address'
# ... Column mappings
user = relationship("User", back_populates="addresses", lazy="raise_on_sql")
이러한 매핑을 사용하면 응용 프로그램이 'lazy loading'에 차단되어 특정 쿼리에 로더 전략을 지정해야 합니다.
u1 = s.execute(select(User)).scalars().first()
u1.addresses
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'
예외는 이 컬렉션을 대신 먼저 로드해야 함을 나타냅니다.
u1 = s.execute(select(User).options(selectinload(User.addresses))).scalars().first()
lazy="raise_on_sql"
옵션은 N : 1 관계에도 현명하게 시도합니다.
위에서 Address.user
속성이 Address
에 로드되지 않았지만 해당 User
객체가 동일한 Session
에 있는 경우 "raiseload"은 오류를 발생시키지 않습니다.
참고
raiseload
를 사용하여 원치 않는 lazy loading 방지
relatonship
에서 lazy loading 방지